package edu.vanderbilt.vshare.multicast;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;

/**
 * class for manipulating, storing, or piecing together a single file
 * @author jedmondson
 *
 * 
 */
public class FileStruct {

	/**
	 * Constructor
	 */
	
	public FileStruct()
	{
		failed = true;
	}

	/**
	 * Constructor for a file with a certain packet size (advanced)
	 * 
	 * @param     filename           name of the file to read
	 * @param     payloadSize        chunk size to divide file into for sending
	 */
	
	public FileStruct(String filename, int payloadSize) {
		this.payloadSize = payloadSize;
		readContents (filename, payloadSize);
	}

	/**
	 * Constructor for a file with a default packet size (recommended)
	 * 
	 * @param     filename           name of the file to read
	 */
	
	public FileStruct (String filename)
	{
		readContents (filename, this.payloadSize);
	}

	/**
	 * reset the failed flag. Should be used if a FileStruct is being reused
	 * from a previously failed FileStruct creation
	 */
	
	public void resetFailed ()
	{
		failed = false;
	}

	/**
	 * check to see if we are working with a valid FileStruct
	 * 
	 * @return             true if this FileStruct is invalid
	 */
	
	public boolean hasFailed ()
	{
		return failed;
	}

	/**
	 * get the range of valid payloads starting from some index. This is useful
	 * for a client application to know how many contiguous packets we have received
	 * and verified. Can be particularly useful to streaming.
	 * 
	 * @param     offset             a valid packet offset
	 * @return                       number of valid packets starting with index
	 */
	
	public synchronized int getValidRange (int offset)
	{
		int range = 0;
		
		for(range = 0; offset + range < contents.length &&
		       contents[offset + range] != null; ++range);
		
		return range;
	}

	/**
	 * Compress the valid payloads into a byte array starting from some packet
	 * offset into the file 
	 * 
	 * @param     offset             a valid packet offset
	 * @return                       contiguous byte array that is complete starting
	 *                               from packet offset
	 */
	
	public byte [] getBytes (int offset)
	{
		// first, get number of payloads that have arrived
		// in the next contiguous set
		int range = getValidRange (offset);
		
		int size = Math.min(range * payloadSize, length);
		
		byte [] total = new byte[size];
		
		if (range > 0)
		{
				int j = 0;
				for (int i = offset; i < offset + range; ++i, j += payloadSize)
					Utility.copy(contents[i], 0, total, j);
			
		}
		
		return total;
	}

	/**
	 * return the incomplete payloads that are still not recorded
	 * 
	 * @return                       number of packets still outstanding for file
	 */
	
	public synchronized int getIncompletes ()
	{
		return contents.length - contentsMap.size();
	}

	/**
	 * return the name of the file
	 * 
	 * @return                       name of the file
	 */
	
	public String getName ()
	{
		return name;
	}

	/**
	 * return the metadata packet for this file
	 * 
	 * @return                       metadata for the file
	 */
	
	public byte [] getMeta ()
	{
		return meta;
	}

	/**
	 * construct what we can from the metadata. Generally metadata tells us
	 * the name and length of a file. We can also sometimes get a valid content type.
	 * 
	 * @param     meta               metadata received from server
	 */
	
	public void constructFromMeta (byte [] meta)
	{
		failed = false;
		String metadata = new String (meta);
		String [] lines = metadata.split("\n");
		
		for (String line: lines)
		{
			String [] pair = line.split(": ");
			if (pair.length > 1 && pair[0] != null && pair[1] != null)
				metaMap.put(pair[0], pair[1]);
		}
		
		try
		{
			// construct what we can from what we know from metadata
			this.length = Integer.parseInt(metaMap.get("length"));
			this.name = metaMap.get("name");

			// packets = number of packets needed to hold the file, plus
			// a meta data packet.
			int packets = length / payloadSize;
			
			// if the length doesn't divide cleanly, we have to add at least
			// 1. We have to add an additional one for the meta packet anyway
			packets += length % payloadSize > 0 ? 2 : 1;
			
			// get the length of the file
			contents = new byte [packets][];
			
			contentsMap = new HashMap <Integer, Boolean > (packets);
			
			Utility.fillNulls (contents);
			
			this.meta = meta;
			
			contents[0] = meta;
			contentsMap.put(-1, true);
		}
		catch (Exception e)
		{
			failed = true;
			return;
		}
	}

	/**
	 * return the number of packets that this file is divided into. This number included
	 * the metadata packet.
	 * 
	 * @return                       number of packets the file is divided into
	 */
	
	public int getPackets ()
	{
		if (contents != null)
			return contents.length;
		
		return 0;
	}

	/**
	 * return the payload for a particular packet index
	 * 
	 * @param     index              a packet index
	 * @return                       if packet has been received and validated or
	 *                               simply read on server, this returns the payload
	 *                               at a given index. Otherwise, returns null.
	 */
	
	public synchronized byte [] getPayload (int index)
	{
		if (failed)
			return null;		
		
		if (index + 1 < contents.length)
		{
			return contents [index + 1];
		}
		else
		{
			return null;
		}
	}

	/**
	 * return the payload for a particular packet index
	 * 
	 * @param     source             a packet index
	 * @return                       if packet has been received and validated or
	 *                               simply read on server, this returns the payload
	 *                               at a given index. Otherwise, returns null.
	 */
	
	public byte [] fillToBoundary (byte [] source)
	{
		byte [] result = new byte [payloadSize];
		
		int i = 0;
		byte zero = 0;
		
		for (i = 0; i < source.length && i < payloadSize; ++i)
			result[i] = source[i];
		
		for (; i < payloadSize; ++i)
			result[i] = zero;
		
		return result;
	}

	/**
	 * sets a payload at the index. Do not use override unless you are an advanced
	 * user. There is no reason to override a payload unless the file has changed.
	 * 
	 * @param     index              a packet index
	 * @param     payload            replacement payload
	 * @param     override           if true, replaces any existing payload
	 */
	
	public synchronized void setPayload (int index, byte [] payload, boolean override)
	{
		if (contents[index + 1] == null || override)
		{
			contents[index + 1] = payload;
			contentsMap.put (index, true);
		}
	}

	/**
	 * return the range of verified packets starting at some index
	 * 
	 * @param     known_index        a packet index
	 * @return                       range of verified packets that start
	 *                               at known_index
	 */
	
	public synchronized int getNextNeededPacketRange (Integer known_index)
	{
		// increment the known index until we find a null or until
		// we are out of bounds.
		for (++known_index; known_index < contents.length &&
		       contents[known_index] != null ; ++known_index);
		
		return getRangeNeeded (known_index);
	}

	/**
	 * return the range of outstanding packets starting at some index
	 * 
	 * @param     unknown_index      a packet index
	 * @return                       range of outstanding packets that start
	 *                               at unknown_index
	 */
	
	public synchronized int getRangeNeeded (int unknown_index)
	{
		int range = 0;
		
		for(range = 0; unknown_index + range < contents.length &&
		       contents[unknown_index + range] == null; ++range);
		
		return range;
	}

	/**
	 * read a file into this FileStruct and divide it up into chunks of
	 * packetSize
	 * 
	 * @param     file               name of file to read
	 * @param     packetSize         packet size to divide the file into
	 * @return                       success if file was read
	 */
	
	public boolean readContents (String file, int packetSize)
	{
		StringBuilder meta = new StringBuilder ();
		File inputFile = new File (file);
		
		if (!inputFile.exists())
		{
			System.out.println ("ERROR: file " + file + " does not exist.");
			failed = true;
			return !failed;
		}
		
		// get the filename and format it to the header file name size
		name = inputFile.getName ();

		// get the content type for metadata
		if (name.endsWith(".jpg"))
		{
			contentType = "image/jpeg";
		}
		else if(name.endsWith(".mpg"))
		{
			contentType = "video/mpeg";
		}
		else if(name.endsWith(".pdf"))
		{
			contentType = "application/pdf";
		}
		else if(name.endsWith(".html"))
		{
			contentType = "text/html";
		}
		else if(name.endsWith(".txt"))
		{
			contentType = "text/plain";
		}
		else if(name.endsWith(".mp4"))
		{
			contentType = "mpeg4";
		}
		else if(name.endsWith(".3gp"))
		{
			contentType = "3gp";
		}
		else if(name.endsWith(".ppt") || name.endsWith(".pptx"))
		{
			contentType = "application/mspowerpoint";
		}
		else
		{
			contentType = "application/octet-stream";
		}	

		length = (int)inputFile.length();
		

		// packets = number of packets needed to hold the file, plus
		// a meta data packet.
		int packets = length / payloadSize;
		
		// if the length doesn't divide cleanly, we have to add at least
		// 1. We have to add an additional one for the meta packet anyway
		packets += length % payloadSize > 0 ? 2 : 1;
		
		// get the length of the file
		contents = new byte [packets][];
		
		contentsMap = new HashMap <Integer, Boolean > (packets);
		
		meta.append("name: ");
		meta.append(name);
		meta.append("\ncontent-type: ");
		meta.append(contentType);
		meta.append("\nlength: ");
		meta.append(length);
		meta.append("\n");
		
		System.out.println ("meta should be " + meta.toString ());
		
		this.meta = fillToBoundary(meta.toString().getBytes ());
		
		System.out.println ("meta length is " + this.meta.length);
		
		contents[0] = this.meta;
		contentsMap.put(-1, true);
		
		try
		{
			// grab a file stream
			FileInputStream fileIn = new FileInputStream (inputFile);
			
			for (int i = 1; i < contents.length; ++i)
			{
				contents[i] = new byte [packetSize];
				int amountRead = fileIn.read(contents[i]);
				
				if (amountRead != packetSize)
					Utility.fillNulls(contents[i], amountRead);
				
				contentsMap.put(i, true);
			}
		}
		catch (IOException e)
		{
			failed = true;
		}
		
		return !failed;
	}

	/**
	 * @return the payloadSize
	 */
	public int getPayloadSize() {
		return payloadSize;
	}

	/**
	 * @param payloadSize the payloadSize to set
	 */
	public void setPayloadSize(int payloadSize) {
		this.payloadSize = payloadSize;
	}

	/**
	 * @return the contentType
	 */
	public String getContentType() {
		return contentType;
	}

	/**
	 * @return the length
	 */
	public int getLength() {
		return length;
	}

	private byte [][] contents;
	private int length;
	private String name;
	private String contentType;

	private boolean failed = false;
	private byte [] meta;
	private HashMap <String, String> metaMap = new HashMap <String, String> ();
	private HashMap <Integer, Boolean> contentsMap;
	private int payloadSize = 16384;
}
